package jdbmaplite.util;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;

public class ClassUtils {

	/**
	 * Get Declared Fields
	 * 
	 * @param clazz
	 *            Class
	 * @return Field[]
	 */
	public static Field[] getDeclaredFields(Class<?> clazz) {
		if (clazz == null) {
			return null;
		}
		List<Field> fields = new ArrayList<Field>();
		Field[] allDeclaredFields = clazz.getDeclaredFields();
		/* ignore synthetic fields */
		for (Field field : allDeclaredFields) {
			if (!field.isSynthetic()) {
				fields.add(field);
			}
		}
		return fields.toArray(new Field[fields.size()]);
	}

	/**
	 * Get Declared Field Names
	 * 
	 * @param clazz
	 *            Class
	 * @return String[]
	 */
	public static String[] getDeclaredFieldNames(Class<?> clazz) {
		Field[] typeFields = getDeclaredFields(clazz);
		if (typeFields == null) {
			return null;
		}
		int typeFieldsSize = typeFields.length;
		String[] typeFieldNames = new String[typeFieldsSize];
		for (int i = 0; i < typeFieldsSize; i++) {
			typeFieldNames[i] = typeFields[i].getName();
		}
		return typeFieldNames;
	}

	/**
	 * Get Declared Field Values
	 * 
	 * @param object
	 *            Object
	 * @return Object[]
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 */
	public static Object[] getDeclaredFieldValues(Object object)
			throws IllegalArgumentException, IllegalAccessException {
		Field[] typeFields = null;
		if (object == null
				|| (typeFields = getDeclaredFields(object.getClass())) == null) {
			return null;
		}
		int typeFieldsSize = typeFields.length;
		Object[] typeFieldValues = new Object[typeFieldsSize];
		for (int i = 0; i < typeFieldsSize; i++) {
			Field field = typeFields[i];
			if (!field.isAccessible()) {
				field.setAccessible(true);
			}
			typeFieldValues[i] = field.get(object);
		}
		return typeFieldValues;
	}

	/**
	 * Is Same Method (for ASM only)
	 * 
	 * @param methodName
	 *            String
	 * @param methodDesc
	 *            String
	 * @param comparedTo
	 *            Method
	 * @return boolean
	 */
	private static boolean isSameMethod(String methodName, String methodDesc,
			String comparedToMethodName, Object... comparedToParameters) {
		if (methodName == null || comparedToMethodName == null
				|| !methodName.equals(comparedToMethodName)) {
			return false;
		}
		int parametersSize = (comparedToParameters != null ? comparedToParameters.length
				: 0);
		Type[] argTypes = Type.getArgumentTypes(methodDesc);
		if (parametersSize != argTypes.length) {
			return false;
		}
		for (int i = 0; i < parametersSize; i++) {
			Class<?> argTypeClass = null;
			try {
				argTypeClass = Class.forName(argTypes[i].getClassName());
			} catch (Exception e) {
				throw new RuntimeException("unknown args classname");
			}
			Object comparedToObject = comparedToParameters[i];
			if (comparedToObject == null && argTypeClass.isPrimitive()) {
				return false;
			} else if (comparedToObject != null
					&& !argTypeClass.isAssignableFrom(comparedToObject
							.getClass())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Get Return CastType (for ASM only)
	 * 
	 * @param desc
	 *            String
	 * @param signature
	 *            String
	 * @return Class{?}
	 */
	private static Class<?> getReturnCastType(String desc, String signature)
			throws ClassNotFoundException {
		Type returnType = Type.getReturnType(desc);
		if (Type.VOID_TYPE.equals(returnType)) {
			return null;
		}
		String returnClassName = returnType.getClassName();
		Class<?> castType = null;
		if (!returnClassName.equals("java.util.List")
				&& !Collection.class.isAssignableFrom(castType = Class
						.forName(returnClassName))) {
			return castType;
		}
		/* if a collection, use it's generic type */
		final String[] genericReturnTypeArray = new String[1];
		new SignatureReader(signature)
				.accept(new SignatureVisitor(Opcodes.ASM4) {
					@Override
					public void visitClassType(String name) {
						genericReturnTypeArray[0] = name;
					}
				});
		return Class.forName(Type.getObjectType(genericReturnTypeArray[0])
				.getClassName());
	}

	/**
	 * Get Declared MethodInfo
	 * 
	 * @param clazz
	 *            Class
	 * @param methodName
	 *            String
	 * @param parameters
	 *            Object...
	 * @return MethodInfo
	 * @throws IOException
	 */
	public static MethodInfo getDeclaredMethodInfo(Class<?> clazz,
			final String methodName, final Object... parameters)
			throws IOException {
		if (clazz == null || methodName == null) {
			return null;
		}
		final MethodInfo methodInfo = new MethodInfo();
		final int parametersSize = (parameters != null ? parameters.length : 0);
		final String[] parameterNames = new String[parametersSize];
		methodInfo.setSimpleMethodName(methodName);
		methodInfo.setParameterNames(parameterNames);

		new ClassReader(clazz.getName()).accept(new ClassVisitor(Opcodes.ASM4) {
			@Override
			public MethodVisitor visitMethod(final int access,
					final String name, final String desc,
					final String signature, final String[] exceptions) {
				MethodVisitor visitor = super.visitMethod(access, name, desc,
						signature, exceptions);
				if (!isSameMethod(name, desc, methodName, parameters)) {
					return visitor;
				}
				/* get castType */
				try {
					methodInfo.setCastType(getReturnCastType(desc, signature));
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
				/* get parameterType */
				return new MethodVisitor(Opcodes.ASM4, visitor) {
					@Override
					public void visitLocalVariable(String name, String desc,
							String signature, Label start, Label end, int index) {
						int i = index - 1;
						// i = index for static method
						// if (static) { i = index; }
						if (i >= 0 && i < parametersSize) {
							parameterNames[i] = name;
						}
						super.visitLocalVariable(name, desc, signature, start,
								end, index);
					}
				};
			}
		}, ClassReader.SKIP_FRAMES);
		return methodInfo;
	}

	/**
	 * Trace Caller
	 * 
	 * @param shiftTraceCount
	 *            int
	 * @return StackTraceElement
	 */
	public static StackTraceElement traceCaller(int shiftTraceCount) {
		if (shiftTraceCount < 0) {
			throw new IllegalArgumentException(
					"shiftTraceCount cannot be a negative number");
		}
		StackTraceElement[] stackTraceArray = Thread.currentThread()
				.getStackTrace();
		return stackTraceArray[shiftTraceCount + 1];
	}

	/**
	 * To Class Resource Path
	 * 
	 * @param clazz
	 *            Class
	 * @return String
	 */
	public static String toClassResourcePath(Class<?> clazz) {
		if (clazz == null) {
			return null;
		}
		StringBuilder ret = new StringBuilder();
		ret.append("/");
		if (clazz != null) {
			ret.append(clazz.getName().replace(".", "/"));
		}
		return ret.toString();
	}

}